package org.fjsei.yewu.aop.hibernate;

import org.fjsei.yewu.filter.Node;
import org.hibernate.Session;
import org.hibernate.search.engine.backend.document.DocumentElement;
import org.hibernate.search.engine.backend.document.IndexFieldReference;
import org.hibernate.search.engine.backend.document.IndexObjectFieldReference;
import org.hibernate.search.engine.backend.document.model.dsl.IndexSchemaObjectField;
import org.hibernate.search.engine.backend.types.Aggregable;
import org.hibernate.search.engine.backend.types.IndexFieldType;
import org.hibernate.search.engine.backend.types.Projectable;
import org.hibernate.search.engine.backend.types.Sortable;
import org.hibernate.search.mapper.orm.HibernateOrmExtension;
import org.hibernate.search.mapper.pojo.bridge.PropertyBridge;
import org.hibernate.search.mapper.pojo.bridge.binding.PropertyBindingContext;
import org.hibernate.search.mapper.pojo.bridge.mapping.programmatic.PropertyBinder;
import org.hibernate.search.mapper.pojo.bridge.runtime.PropertyBridgeWriteContext;
import org.hibernate.search.mapper.pojo.model.PojoModelProperty;

import jakarta.persistence.EntityManager;
import jakarta.persistence.Tuple;
import jakarta.persistence.TypedQuery;
import java.util.UUID;

/** 只需要索引关联Unit对象的ID+name字段，代替掉@IndexedEmbedded(includePaths = {"id","company.name","person.name"} )
 * 目标是提高性能：不要多余的sql查询语句，没必要查询每一个具体关联对象表。
 * 对象属性桥接 hibernateSearch；
 * 自动重建索引时刻，发现关联查询没啥用处的查了很多字段。如何能够避免，提高性能！
 * 把Isp的servu字段注解修改，从@IndexedEmbedded(includePaths = {"id","company.name","person.name"} )
 * 直接变更为  @PropertyBinding(binder = @PropertyBinderRef(type = UunodeIdBinder.class)) 然后重建索引 也不报错啊。
 * 【文档】HS参考   https://docs.jboss.org/hibernate/search/6.2/reference/en-US/html_single/#section-field-bridge
 * 经过MySql对UUID改Long的ID类型做测试证明可行的。就是HS 6.2.0.Alpha1版对小强数据库还不能支持，outbox-polling的事件表无法归空=不停报错。
 * outbox-polling同步措施，实际有pulse_interval周期，有小延迟(配置)，实体修改后触发关联实体真正的同步ES索引滞后的，并不是一个事务内。
 * 支持Unit.id ID类型Long UUID;
 * */
public class UnitNameBinder implements PropertyBinder {
    //目标索引字段名。默认，下面替换为注解的实际字段名。
    private String fieldName = "unit";

    public UnitNameBinder fieldName(String fieldName) {
        this.fieldName = fieldName;
        return this;
    }

    @Override
    public void bind(PropertyBindingContext context) {
        //【特别注意】确保关联属性被修改时，涉及ES索引也能够修改。 Unit-> . ->嵌套两层了,...name,依靠refactor不能自动改动字符串中的字段名。
        context.dependencies()
                .use( "id" )
                .use( "company.name" )
                .use( "person.name" );

//        IndexSchemaObjectField unitField = context.indexSchemaElement()
//                .objectField( this.fieldName );

        PojoModelProperty bridgedElement = context.bridgedElement();
        IndexSchemaObjectField unitField = context.indexSchemaElement()       //挂接关联索引根出处
                .objectField( bridgedElement.name() );          //实际字段名称；
        //针对Unit:"id"字段;
        PojoModelProperty  idProperty= bridgedElement.property("id");
        boolean isLongType= idProperty.isAssignableTo(Long.class);

        /*修改注解以后：启动时刻Hibernate Search检查旧的ES索引的字段定义和新的要求不一致，导致启动报错！只能刪除旧索引！
          field 'dev.id': attribute 'doc_values': - Invalid value. Expected 'true', actual is 'false'；
          默认产生是"doc_values": false； 不⽀持排序了，需要打开doc_values才⾏；加.aggregable(Aggregable.YES).sortable(Sortable.YES)对应于doc_values=true;
         当 doc_values 为 fasle 时，无法基于该字段排序、聚合、在脚本中访问字段值，当 doc_values=true 时，ES 会增加一个相应的正排索引。
        * */
        IndexFieldType<?> idFieldType = null;          //<?>对应着(IndexFieldReference<？>)的类型；
        if(isLongType){
            idFieldType =(IndexFieldType<Long>)( context.typeFactory().asLong()
                    .aggregable(Aggregable.YES).sortable(Sortable.YES)
                    .projectable(Projectable.YES)
                    .toIndexFieldType() );
        }
        else {
            idFieldType =(IndexFieldType<String>)( context.typeFactory().asString()
                    .aggregable(Aggregable.YES).sortable(Sortable.YES)
                    .projectable(Projectable.YES)
                    .toIndexFieldType() );
        }
        /*针对ES底层，自动适配.String 转映射 "text": if an analyzer is defined, keyword otherwise。
          报错both analyzer 'standard' and sorts are enabled. Sorts aggregable are not supported on analyzed fields.
          default analyzer,只能刪除旧的索引！報錯attribute 'analyzer': Expected 'standard', actual is 'null'  .analyzer("standard")
        * */
        IndexFieldType<String> nameFieldType = context.typeFactory().asString().analyzer("default")
                .projectable(Projectable.YES)
                .toIndexFieldType();
        //第二层挂接到上一层的：
        IndexSchemaObjectField cmpobjField = unitField.objectField( "company" );
        IndexSchemaObjectField perobjField = unitField.objectField( "person" );
        //嵌套的两层也需要在这里就得构建定义： 底下.field( "person.name",报错: field names cannot contain a dot ('.').
        //不能改关系:把person company和unit.id并列的结构，spring-data-elasticsearch搜索不能借用Unit类型报错No converter found capable of converting from type [java.lang.String] to type [md.cm.base.Company]
        //需要和Unit-内部定义的嵌套Company Person关系保持一致。
        if(isLongType){
            context.bridge( Node.class, new Bridge<Long>(
                    unitField.toReference(),
                    //可以加多个： 对象字段有下面嵌套的有多个关联的字段
                    unitField.field( "id", idFieldType ).toReference(),
                    Long.class,
                    cmpobjField.toReference(),
                    perobjField.toReference(),
                    cmpobjField.field( "name", nameFieldType ).toReference(),
                    perobjField.field( "name", nameFieldType ).toReference()) );
        }
        else {
            context.bridge( Node.class, new Bridge<UUID>(
                    unitField.toReference(),
                    //可以加多个： 对象字段有下面嵌套的有多个关联的字段
                    unitField.field( "id", idFieldType ).toReference(),
                    UUID.class,
                    cmpobjField.toReference(),
                    perobjField.toReference(),
                    cmpobjField.field( "name", nameFieldType ).toReference(),
                    perobjField.field( "name", nameFieldType ).toReference()) );
        }
    }

/*若是输出索引对象字段有下面嵌套的有多个关联的字段：需要上面和下面配套多个参数的。
private Bridge(IndexObjectFieldReference unitField,
                IndexFieldReference<BigDecimal> totalField,
                IndexFieldReference<BigDecimal> booksField)
底下的implements PropertyBridge<Uunode> { 这里的泛型<Uunode>是依据于被注解实体字段的类型的。
* */
    @SuppressWarnings("rawtypes")
    private static class Bridge<T>  implements PropertyBridge<Node> {

        private final IndexObjectFieldReference unitField;       //关联对象字段本身
        private final IndexFieldReference<?> unitIdField;       //关联对象字段底下的多个字段
        //第二层次的和关联节点：
        private final IndexObjectFieldReference cmpobjField;
        private final IndexObjectFieldReference perobjField;
        private final IndexFieldReference<String> companyNameField;
        private final IndexFieldReference<String> personNameField;
        //ID的类型 Long 或 UUID
        private final Class<T>  idType;

//        @Resource             没法注入
//        ApplicationContext applicationContext;        也无法注入资源XxxRepository !!
        private Bridge(IndexObjectFieldReference unitField,
                       IndexFieldReference<?> unitIdField,
                       Class<T> idType,
                       IndexObjectFieldReference cmpobjField,
                       IndexObjectFieldReference perobjField,
                       IndexFieldReference<String> companyNameField,
                       IndexFieldReference<String> personNameField) {
            this.unitField = unitField;
            this.unitIdField = unitIdField;
            this.idType = idType;
            this.cmpobjField = cmpobjField;
            this.perobjField = perobjField;
            this.companyNameField = companyNameField;
            this.personNameField = personNameField;
        }

        /*针对是关联对象且 fetch= FetchType.LAZY)的才有意义的。 Unit类型的字段就不会自动查询关联对象了，提高性能。
         无法注入XxxRepository；不能用正常用的查询接口方法。
         只找到这个方式 jpql形式。有缺点：字符串SQL语句出错，运行前可能无法提示错误。不改造默认实体装载的语句太烂长，浪费数据库查询能力。
        * */
        @Override
        public void write(DocumentElement target, Node bridgedElement, PropertyBridgeWriteContext context) {
            Session session = context.extension( HibernateOrmExtension.get() ).session();
            //Node lineItems = (Node) bridgedElement;   这里最为关键：类型决定了如何提取，是否需要查询关联数据库表。节省JPA关联实体load的必要性！但是断点单步调试的会主动加载部分。
            //必须是实际的 Uunodel类型 bridgedElement :运行期才报错的。
//            UUID unitId= null!=bridgedElement? (UUID) bridgedElement.getId() : null;
            T unitId= null!=bridgedElement? (T) bridgedElement.getId() : null;
            if(null==unitId)    return;

            //这里只好用JPQL语法了，native语句更不好的； #不使用隐式内连接；而是使用显式左外连接！
            //【注意】 Unit类的名字修改，可能不会refactor自动同步修订这里的JPQL语句的，要手动改同步的！
            String jpql="select c.name as cname, p.name as pname from Unit u left join u.company c left join u.person p where u.id=:id";
            //用这自定义的查询：就可以避免自动生成的实体装载的SQL语句的冗长，查了一堆没用的字段，关联了一堆实体表，司机对于当前我这里函数真的毫无作用啊，大大提高性能。
            //<R> Query<R> createQuery(String queryString, Class<R> resultClass);  ：这个位置就是没法用普通的Repository Units方式啊。也没法投影interface去查的。
            TypedQuery<Tuple>  simpleQuery= ((EntityManager) session).createQuery(jpql, Tuple.class);
            simpleQuery.setParameter("id",unitId);
            Tuple  rowDat= simpleQuery.getSingleResult();
            String companyName= (String) rowDat.get(0);
            String personName= (String) rowDat.get(1);

            //这句报错没法用 Cannot create TypedQuery for query with more than one return using requested result type [org.fjsei.yewu.filter.UnitName]
            //Query<UnitName> simpleQuery= session.createQuery(jpql, UnitName.class); .setParameter("id",unitId); UnitName simpleQuery.getSingleResult();

            DocumentElement top = target.addObject( this.unitField );
            if(Long.class==this.idType){
                top.addValue((IndexFieldReference<Long>) this.unitIdField, (Long)unitId );
            }else {
                top.addValue((IndexFieldReference<String>) this.unitIdField, unitId.toString() );
            }
            //IDEA调试单步运行 这里设断点的 很可能会出现Unit实体查询sql,而非调试却没有sql语句的。 bridgedElement隐藏就是Unit类型的所以自动查关联。
//            var companyName= null!=bridgedElement? (null!=bridgedElement.getCompany()? bridgedElement.getCompany().getName(): null) : null;
//            var personName= null!=bridgedElement? (null!=bridgedElement.getPerson()? bridgedElement.getPerson().getName(): null) : null;

            if(null!=companyName){
                DocumentElement topCmp =top.addObject(this.cmpobjField);
                topCmp.addValue( this.companyNameField,  companyName);
            }
            else if(null!=personName){
                DocumentElement topPer =top.addObject(this.perobjField);
                topPer.addValue( this.personNameField,  personName);
            }
        }
    }
}


/*旧的， 没采用UnitNameBinder的注解方式：生成索引servu定义,嵌套两层对象：
"servu": {
          "dynamic": "strict",
          "properties": {
            "company": {
              "dynamic": "strict",
              "properties": {
                "name": {
                  "type": "text"
                }
              }
            },
            "id": {
              "type": "keyword"
            },
            "person": {
              "dynamic": "strict",
              "properties": {
                "name": {
                  "type": "text"
                }
              }
            }
          }
        }
 针对ES服务?
  "address": {
          "type": "text",
          "analyzer": "standard"
        },
* */